Writing Unit Tests Using JUnit
Course: Advanced Java Training
Topic: Writing Unit Tests Using JUnit
Course Link: Advanced Java Training – JustAcademy
Demo Link: Register for a Course Demo
Website: JustAcademy Official Website
1. Introduction to Unit Testing
Unit testing is a software testing technique used to verify individual units of an application, such as methods or classes. It helps developers confirm that code behaves as expected and continues to work correctly after changes.
JUnit is a popular testing framework for Java that provides annotations, assertions, test lifecycle methods, and parameterized testing capabilities. It is widely used to automate tests during development and in continuous integration pipelines.
For example, if a calculator method adds two numbers, a unit test can verify that adding 5 and 3 produces 8. If the implementation later changes incorrectly, the test can detect the defect.
Learning Objectives
- Understand unit testing and the role of JUnit in Java development.
- Set up JUnit in a Maven or Gradle project.
- Write test classes and test methods.
- Use JUnit annotations and assertions.
- Test exceptions, boundary values, and invalid inputs.
- Write parameterized tests for multiple scenarios.
- Use Mockito to isolate dependencies.
- Test service classes in Spring Boot applications.
- Execute tests through an IDE or build tool.
- Apply testing best practices to create reliable automated tests.
2. What Is JUnit?
JUnit is an open-source Java testing framework that allows developers to create and execute automated tests. Modern Java projects commonly use JUnit 5, whose architecture includes the JUnit Platform, JUnit Jupiter, and JUnit Vintage.
Important Features
- Annotations: Identify test methods and lifecycle methods.
- Assertions: Verify expected results and conditions.
- Test lifecycle: Run setup and cleanup logic around tests.
- Parameterized tests: Run the same test with different inputs.
- Exception testing: Verify expected exceptions.
- IDE support: Execute tests directly from development environments.
- Build integration: Run automated tests using Maven or Gradle.
3. What Is a Unit Test?
A unit test checks a small, identifiable part of an application in a controlled environment. It focuses on one behavior and aims to identify errors without requiring the entire application to run.
Example
public class Calculator {
public int add(int a, int b) {
return a + b;
}
}
A unit test can call add(5, 3) and verify that the result is 8.
Unit Testing Versus Other Testing Types
| Testing Type |
Purpose |
Example |
| Unit testing |
Tests individual units in isolation. |
Testing a calculator method. |
| Integration testing |
Verifies that multiple components work together. |
Testing a service with a database. |
| System testing |
Tests the complete application. |
Testing a full shopping application. |
| Acceptance testing |
Verifies business requirements and user expectations. |
Testing a checkout workflow against requirements. |
4. Understanding JUnit 5 Architecture
- JUnit Platform: Provides the foundation for launching testing frameworks and test engines.
- JUnit Jupiter: Provides the programming model, annotations, and extension support for modern JUnit tests.
- JUnit Vintage: Supports running tests written with older JUnit versions when the appropriate engine is configured.
For new tests, developers generally use JUnit Jupiter and annotations from the org.junit.jupiter.api package.
5. Setting Up JUnit in a Java Project
JUnit can be added to a project using Maven or Gradle. The following examples use JUnit Jupiter.
5.1 Maven Configuration
Add this dependency inside the <dependencies> section of your Maven pom.xml file. The version shown is an example; choose a version compatible with your Java and build configuration.
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.12.2</version>
<scope>test</scope>
</dependency>
Ensure your Maven Surefire Plugin version supports the JUnit Platform so that tests can be discovered and executed correctly.
5.2 Gradle Configuration
dependencies {
testImplementation("org.junit.jupiter:junit-jupiter:5.12.2")
}
tasks.test {
useJUnitPlatform()
}
Use the version managed by your project's dependency-management configuration where possible.
5.3 Standard Project Structure
my-java-project/
├── src/
│ ├── main/
│ │ └── java/com/example/Calculator.java
│ └── test/
│ └── java/com/example/CalculatorTest.java
├── pom.xml
└── build.gradle
Production code belongs in src/main/java, while test code normally belongs in src/test/java.
6. Writing Your First JUnit Test
Let's create a calculator class and test its addition method.
Production Class: Calculator.java
package com.example;
public class Calculator {
public int add(int a, int b) {
return a + b;
}
}
Test Class: CalculatorTest.java
package com.example;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class CalculatorTest {
@Test
void shouldAddTwoNumbers() {
Calculator calculator = new Calculator();
int result = calculator.add(5, 3);
assertEquals(8, result);
}
}
Explanation
@Test marks the method as a test method.
- The test creates an instance of
Calculator.
- The test calls
add(5, 3).
- The result is stored in the
result variable.
assertEquals(8, result) verifies the expected result.
If the actual result equals 8, the assertion passes. If the implementation returns another value, the test fails and reports the mismatch.
7. The Arrange, Act, Assert Pattern
The Arrange, Act, Assert (AAA) pattern is a common way to organize unit tests.
| Step |
Description |
Example |
| Arrange |
Prepare objects and input values. |
Create a calculator and define inputs. |
| Act |
Execute the behavior being tested. |
Call the addition method. |
| Assert |
Verify the expected outcome. |
Check that the result is 8. |
@Test
void shouldCalculateTotalPrice() {
// Arrange
PriceCalculator calculator = new PriceCalculator();
// Act
double total = calculator.calculateTotal(100.0, 3);
// Assert
assertEquals(300.0, total, 0.001);
}
The delta parameter in the floating-point assertion allows a small tolerance when comparing decimal values. For financial calculations, consider using BigDecimal with a defined rounding policy.
8. Important JUnit Annotations
| Annotation |
Purpose |
@Test |
Marks a test method. |
@BeforeEach |
Runs before each test method. |
@AfterEach |
Runs after each test method. |
@BeforeAll |
Runs once before all tests in a test class. |
@AfterAll |
Runs once after all tests in a test class. |
@DisplayName |
Sets a readable name for a test or test class. |
@Disabled |
Temporarily disables a test or test class. |
@Nested |
Groups related tests in nested classes. |
@ParameterizedTest |
Runs a test with multiple input sets. |
@RepeatedTest |
Repeats a test a specified number of times. |
@Tag |
Labels tests for selective execution. |
@Timeout |
Sets a time limit for a test. |
@TestFactory |
Creates dynamic tests at runtime. |
9. Using @BeforeEach and @AfterEach
@BeforeEach is useful for preparing a fresh object before every test. @AfterEach can release resources or reset state after each test.
package com.example;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class CalculatorLifecycleTest {
private Calculator calculator;
@BeforeEach
void setUp() {
calculator = new Calculator();
}
@AfterEach
void tearDown() {
calculator = null;
}
@Test
void shouldAddNumbers() {
assertEquals(10, calculator.add(4, 6));
}
@Test
void shouldAddNegativeNumbers() {
assertEquals(-5, calculator.add(-2, -3));
}
}
The setup method runs before every test, so each test receives a newly initialized calculator.
10. Using @BeforeAll and @AfterAll
@BeforeAll runs once before the test methods in a class, while @AfterAll runs once after them. These methods are useful for shared setup and cleanup operations.
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
class TestEnvironment {
@BeforeAll
static void initializeEnvironment() {
System.out.println("Initialize test environment");
}
@AfterAll
static void cleanUpEnvironment() {
System.out.println("Clean up test environment");
}
@Test
void sampleTest() {
// Add test logic here.
}
}
By default, these lifecycle methods must be static. With @TestInstance(TestInstance.Lifecycle.PER_CLASS), they may be instance methods. Handle shared mutable state carefully, particularly when tests execute concurrently.
11. Understanding JUnit Assertions
Assertions verify whether actual behavior matches the expected result. If an assertion fails, JUnit reports a test failure.
| Assertion |
Purpose |
Example |
assertEquals() |
Checks equality. |
assertEquals(10, result) |
assertNotEquals() |
Checks that values differ. |
assertNotEquals(0, result) |
assertTrue() |
Checks that a condition is true. |
assertTrue(age >= 18) |
assertFalse() |
Checks that a condition is false. |
assertFalse(name.isEmpty()) |
assertNull() |
Checks that a value is null. |
assertNull(value) |
assertNotNull() |
Checks that a value is not null. |
assertNotNull(customer) |
assertSame() |
Checks whether references point to the same object. |
assertSame(first, second) |
assertNotSame() |
Checks whether references point to different objects. |
assertNotSame(first, second) |
assertThrows() |
Checks whether an expected exception is thrown. |
assertThrows(IllegalArgumentException.class, action) |
assertAll() |
Executes multiple assertions and reports failures together. |
Validate several DTO properties. |
assertIterableEquals() |
Compares iterable contents and order. |
Compare two lists. |
assertTimeout() |
Checks whether an operation completes within a specified duration. |
Verify a time constraint. |
Example: Multiple Assertions
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class CustomerTest {
@Test
void shouldValidateCustomerDetails() {
String name = "Aarav";
int age = 25;
assertAll(
() -> assertEquals("Aarav", name),
() -> assertEquals(25, age),
() -> assertTrue(age > 18)
);
}
}
assertAll() executes the supplied assertions and reports their failures together, which can make diagnosing multiple field mismatches easier.
12. Testing Exceptions With assertThrows()
Methods may throw exceptions when they receive invalid input or cannot complete an operation. The assertThrows() method verifies the expected exception type.
Production Code
public class DivisionService {
public int divide(int dividend, int divisor) {
if (divisor == 0) {
throw new IllegalArgumentException(
"Divisor must not be zero"
);
}
return dividend / divisor;
}
}
Unit Test
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
class DivisionServiceTest {
@Test
void shouldDivideNumbers() {
DivisionService service = new DivisionService();
assertEquals(4, service.divide(20, 5));
}
@Test
void shouldRejectDivisionByZero() {
DivisionService service = new DivisionService();
IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class,
() -> service.divide(10, 0)
);
assertEquals(
"Divisor must not be zero",
exception.getMessage()
);
}
}
The first test verifies successful division. The second checks both the exception type and its message.
13. Testing Edge Cases
Edge cases are inputs or conditions near the boundaries of a method's expected behavior. They help detect defects that ordinary examples might miss.
- Zero values.
- Negative values where applicable.
- Empty strings and collections.
- Null inputs when permitted by the API.
- Minimum and maximum allowed values.
- Invalid formats.
- Missing records.
- Duplicate records or repeated operations.
Example: Name Validation
public class NameValidator {
public boolean isValid(String name) {
return name != null && !name.trim().isEmpty();
}
}
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class NameValidatorTest {
private final NameValidator validator = new NameValidator();
@Test
void shouldAcceptNonEmptyName() {
assertTrue(validator.isValid("Priya"));
}
@Test
void shouldRejectEmptyName() {
assertFalse(validator.isValid(""));
}
@Test
void shouldRejectWhitespaceOnlyName() {
assertFalse(validator.isValid(" "));
}
@Test
void shouldRejectNullName() {
assertFalse(validator.isValid(null));
}
}
14. Parameterized Tests
Parameterized tests execute the same test logic with multiple input values. They reduce duplicated code and make it easier to verify different cases consistently.
Example Using @ValueSource
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.assertTrue;
class PositiveNumberTest {
@ParameterizedTest
@ValueSource(ints = {1, 2, 5, 10, 100})
void shouldIdentifyPositiveNumbers(int number) {
assertTrue(number > 0);
}
}
Ensure that JUnit Jupiter parameterized-test support is available in your test dependencies.
Example Using @CsvSource
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
class AdditionParameterizedTest {
@ParameterizedTest
@CsvSource({
"2, 3, 5",
"5, 5, 10",
"-2, 2, 0",
"10, 0, 10"
})
void shouldAddNumbers(int a, int b, int expected) {
Calculator calculator = new Calculator();
assertEquals(expected, calculator.add(a, b));
}
}
Each CSV row supplies two inputs and one expected result. JUnit executes the method for every row.
15. Repeated Tests and Display Names
@RepeatedTest runs a test multiple times. It is useful for checking consistent behavior, although it does not replace dedicated concurrency or reliability tests.
import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.DisplayName;
import static org.junit.jupiter.api.Assertions.assertEquals;
class RepeatedCalculatorTest {
@RepeatedTest(3)
@DisplayName("Addition should remain consistent")
void additionShouldRemainConsistent() {
Calculator calculator = new Calculator();
assertEquals(7, calculator.add(3, 4));
}
}
@DisplayName makes test reports more readable by providing descriptive labels.
16. Grouping Tests With @Nested
@Nested allows related tests to be organized inside a parent test class.
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class CalculatorGroupedTest {
private final Calculator calculator = new Calculator();
@Nested
class AdditionTests {
@Test
void shouldAddPositiveNumbers() {
assertEquals(7, calculator.add(3, 4));
}
@Test
void shouldAddNegativeNumbers() {
assertEquals(-7, calculator.add(-3, -4));
}
}
}
Use nested classes when they improve clarity. Avoid unnecessary nesting that makes tests harder to navigate.
17. Testing Collections and Objects
Java applications often return collections, DTOs, and domain objects. Tests should verify meaningful contents and properties rather than checking only that an object exists.
import java.util.List;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertIterableEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
class ProductListTest {
@Test
void shouldReturnExpectedProductNames() {
List<String> actual = List.of("Laptop", "Phone", "Tablet");
List<String> expected = List.of("Laptop", "Phone", "Tablet");
assertIterableEquals(expected, actual);
assertEquals(3, actual.size());
}
}
assertIterableEquals() compares the elements in order. If ordering is not important, use assertions appropriate to the collection's intended behavior.
18. Introduction to Mockito
Mockito is a mocking framework commonly used with JUnit. It lets developers create mock dependencies, define their behavior, and verify interactions with the class being tested.
| Mockito Operation |
Purpose |
mock() |
Creates a mock object. |
when() |
Defines behavior for a mock method. |
thenReturn() |
Specifies a return value. |
thenThrow() |
Specifies an exception. |
verify() |
Verifies that an interaction occurred. |
never() |
Verifies that an interaction did not occur. |
Example: Service With a Mock Repository
package com.example;
import java.util.Optional;
public record Product(Long id, String name) {
}
interface ProductRepository {
Optional<Product> findById(Long id);
}
class ProductService {
private final ProductRepository repository;
ProductService(ProductRepository repository) {
this.repository = repository;
}
public Product getProduct(Long id) {
return repository.findById(id)
.orElseThrow(() ->
new IllegalArgumentException("Product not found")
);
}
}
package com.example;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.verify;
class ProductServiceTest {
@Test
void shouldReturnProductWhenItExists() {
ProductRepository repository = mock(ProductRepository.class);
Product expected = new Product(1L, "Laptop");
when(repository.findById(1L))
.thenReturn(java.util.Optional.of(expected));
ProductService service = new ProductService(repository);
Product actual = service.getProduct(1L);
assertEquals(expected, actual);
verify(repository).findById(1L);
}
@Test
void shouldThrowExceptionWhenProductDoesNotExist() {
ProductRepository repository = mock(ProductRepository.class);
when(repository.findById(99L))
.thenReturn(java.util.Optional.empty());
ProductService service = new ProductService(repository);
assertThrows(
IllegalArgumentException.class,
() -> service.getProduct(99L)
);
verify(repository).findById(99L);
}
}
Add a compatible Mockito dependency to your test configuration. In Spring Boot projects, spring-boot-starter-test commonly provides JUnit Jupiter and Mockito for test use.
The repository is mocked, so the test does not connect to a real database. It verifies the service's behavior with controlled dependency results.
19. Using Mockito Annotations
Mockito annotations can reduce repetitive setup. The JUnit Jupiter integration initializes them when the test class uses @ExtendWith(MockitoExtension.class).
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class ProductServiceMockitoTest {
@Mock
private ProductRepository repository;
@InjectMocks
private ProductService service;
@Test
void shouldReturnProduct() {
Product expected = new Product(1L, "Laptop");
when(repository.findById(1L))
.thenReturn(java.util.Optional.of(expected));
assertEquals(expected, service.getProduct(1L));
}
}
@Mock creates a mock dependency.
@InjectMocks asks Mockito to inject available mocks into the class being tested.
@ExtendWith(MockitoExtension.class) integrates Mockito with JUnit Jupiter.
20. Writing Unit Tests for Spring Boot Applications
Spring Boot provides testing support through spring-boot-starter-test, which commonly includes JUnit Jupiter, Mockito, Spring Test, and other testing utilities.
Maven Dependency
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
Use the version managed by your Spring Boot dependency management when available.
Testing Approaches
| Approach |
What It Tests |
Typical Use |
| Plain JUnit |
Java logic without starting Spring. |
Testing calculations and validation. |
| Mockito-based unit test |
Business logic with mocked dependencies. |
Testing a service without a real database. |
@WebMvcTest |
A selected Spring MVC web layer. |
Testing controller request and response behavior. |
@DataJpaTest |
JPA persistence components. |
Testing repository queries and entity persistence. |
@SpringBootTest |
A broader Spring application context. |
Integration tests requiring application wiring. |
Use plain JUnit for isolated logic whenever possible. Starting the full Spring application context for every test adds overhead and is often unnecessary for simple unit tests.
21. Running JUnit Tests
Running Tests in an IDE
- Open the project in your Java IDE.
- Ensure the JUnit dependencies are available.
- Open a test class.
- Select the IDE's Run Test action.
- Review the results and investigate failures.
Running Tests With Maven
mvn test
If tests are not discovered, check the test directory, naming conventions, Surefire configuration, and JUnit Platform compatibility.
Running Tests With Gradle
./gradlew test
On Windows, use:
gradlew.bat test
Review the generated test reports to see which tests passed, failed, or were skipped.
22. Understanding Test Results
| Status |
Meaning |
Next Step |
| Passed |
The test completed without assertion failures or unexpected errors. |
Continue checking other scenarios. |
| Failed |
An assertion did not match the expected result. |
Compare expected and actual values and review the implementation. |
| Error |
An unexpected exception or execution problem occurred. |
Inspect the stack trace and test setup. |
| Skipped or disabled |
The test did not execute normally. |
Check whether it was intentionally disabled or conditionally skipped. |
A failure may reveal a production defect, an incorrect expectation, or a problem in the test setup. Investigate before changing the assertion.
23. Common JUnit Testing Mistakes
| Mistake |
Why It Is a Problem |
Solution |
| Testing unrelated behaviors in one method. |
Failures are harder to diagnose. |
Write focused tests for individual behaviors. |
| Using vague test names. |
Reports provide little diagnostic information. |
Describe the expected behavior clearly. |
| Relying on execution order. |
Tests may fail when run individually. |
Make tests independent. |
| Testing only successful cases. |
Boundary conditions remain untested. |
Include invalid inputs and exception cases. |
| Using real external services for every unit test. |
Tests become slow and infrastructure-dependent. |
Mock dependencies in unit tests and use separate integration tests. |
| Making assertions too weak. |
Incorrect behavior may go unnoticed. |
Verify meaningful results and side effects. |
| Ignoring failures. |
Defects remain undetected. |
Investigate and resolve the underlying cause. |
| Excessive mocking. |
Tests can verify implementation details instead of behavior. |
Mock selectively and focus on observable outcomes. |
24. Best Practices for Writing Unit Tests
- Keep tests focused on one behavior.
- Use descriptive names that explain the expected outcome.
- Follow Arrange, Act, Assert to improve readability.
- Make tests deterministic and independent.
- Cover normal cases, boundary conditions, and failure scenarios.
- Use mocks to isolate external dependencies where appropriate.
- Avoid relying on internal implementation details unnecessarily.
- Run tests frequently during development.
- Integrate automated tests into the build pipeline.
- Use integration tests to verify interactions with databases, APIs, and Spring configuration.
- Keep test data isolated and avoid production credentials.
- Update tests when business requirements change.
25. Practical Project: Employee Management Unit Tests
Create a small employee management module and write unit tests for employee validation and bonus calculation.
Project Requirements
- Create an employee model with ID, name, and salary.
- Create an employee service.
- Reject blank employee names.
- Reject negative salaries.
- Calculate bonuses using a percentage.
- Test valid and invalid input values.
- Use parameterized tests for several bonus calculations.
- Use Mockito if the service depends on an employee repository.
Employee Model
package com.example;
public record Employee(Long id, String name, double salary) {
}
Employee Service
package com.example;
public class EmployeeService {
public Employee createEmployee(Long id, String name, double salary) {
if (name == null || name.trim().isEmpty()) {
throw new IllegalArgumentException("Name is required");
}
if (salary < 0) {
throw new IllegalArgumentException("Salary cannot be negative");
}
return new Employee(id, name.trim(), salary);
}
public double calculateBonus(double salary, double percentage) {
if (salary < 0) {
throw new IllegalArgumentException("Salary cannot be negative");
}
if (percentage < 0 || percentage > 100) {
throw new IllegalArgumentException("Invalid bonus percentage");
}
return salary * percentage / 100;
}
}
Employee Service Test
package com.example;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
class EmployeeServiceTest {
private final EmployeeService service = new EmployeeService();
@Test
void shouldCreateEmployeeWithValidDetails() {
Employee employee = service.createEmployee(1L, " Neha ", 50000);
assertEquals(1L, employee.id());
assertEquals("Neha", employee.name());
assertEquals(50000, employee.salary(), 0.001);
}
@Test
void shouldRejectBlankEmployeeName() {
assertThrows(
IllegalArgumentException.class,
() -> service.createEmployee(1L, " ", 50000)
);
}
@Test
void shouldRejectNegativeSalary() {
assertThrows(
IllegalArgumentException.class,
() -> service.createEmployee(1L, "Neha", -1000)
);
}
@ParameterizedTest
@CsvSource({
"50000, 10, 5000",
"40000, 5, 2000",
"60000, 0, 0"
})
void shouldCalculateBonus(
double salary,
double percentage,
double expectedBonus) {
assertEquals(
expectedBonus,
service.calculateBonus(salary, percentage),
0.001
);
}
}
Project Completion Checklist
- Employee validation rules are covered by tests.
- Invalid salary values produce the expected exception.
- Bonus calculations are tested with multiple input combinations.
- Tests run independently and repeatedly.
- Test reports show failures clearly.
- The project can execute tests through the chosen build tool.
26. Interview Questions and Answers
Q1. What is JUnit?
Answer: JUnit is a Java testing framework that supports automated tests through annotations, assertions, lifecycle methods, and extensions.
Q2. What is unit testing?
Answer: Unit testing verifies an individual unit of application code in isolation to confirm that it behaves according to its requirements.
Q3. What is the difference between JUnit 4 and JUnit 5?
Answer: JUnit 5 introduced a modular architecture with the Platform, Jupiter, and Vintage components. Jupiter provides the modern programming model and extension support.
Q4. What does @Test do?
Answer: It marks a method as a test method that the JUnit engine can discover and execute.
Q5. What is the difference between @BeforeEach and @BeforeAll?
Answer: @BeforeEach runs before each test method, while @BeforeAll runs once before all tests in the class.
Q6. What is an assertion?
Answer: An assertion checks whether an actual result matches an expected value or condition.
Q7. What is assertThrows()?
Answer: It verifies that an operation throws an exception of the expected type.
Q8. What are parameterized tests?
Answer: They execute the same test logic with multiple input values, reducing duplicated test code.
Q9. What is Mockito?
Answer: Mockito creates mock objects, defines their behavior, and verifies interactions with dependencies.
Q10. What is the difference between a mock and a real object?
Answer: A real object executes its implementation. A mock is a test double whose behavior can be controlled and whose interactions can be verified.
Q11. What is the AAA pattern?
Answer: AAA means Arrange, Act, Assert. It organizes a test into setup, execution, and verification.
Q12. What is the difference between unit and integration testing?
Answer: Unit testing focuses on isolated behavior, while integration testing checks whether multiple components work together.
Q13. Why should tests be independent?
Answer: Independent tests can run in any order and are less likely to fail because of state changes made by other tests.
Q14. How do you run JUnit tests in Maven?
Answer: Run mvn test after configuring compatible JUnit dependencies and test execution plugins.
Q15. What makes a good unit test?
Answer: A good unit test is focused, readable, deterministic, independent, and checks meaningful behavior across normal and failure scenarios.
27. Quick Revision Checklist
- JUnit automates testing in Java projects.
- JUnit 5 includes the Platform, Jupiter, and Vintage components.
@Test marks a test method.
@BeforeEach and @AfterEach run around each test method.
@BeforeAll and @AfterAll run around the test class lifecycle.
- Assertions compare actual results with expected results.
assertThrows() verifies exception behavior.
@ParameterizedTest tests multiple input combinations.
- Mockito isolates dependencies during unit tests.
- Maven and Gradle can execute tests automatically.
- Unit tests should be deterministic and independent.
- Integration tests are needed for component interactions and infrastructure behavior.
28. Official References and Further Learning
Conclusion
Writing unit tests using JUnit is an essential skill for Java developers. JUnit provides a structured way to verify application behavior, while annotations, assertions, parameterized tests, and exception testing help cover a wide range of scenarios. Mockito can isolate dependencies so that service logic can be tested without relying on external systems.
By following the Arrange, Act, Assert pattern, writing descriptive and independent tests, and running tests regularly through an IDE or build tool, developers can identify defects earlier and maintain reliable Java applications. Begin with simple methods, progress to business rules and dependency-based services, and add integration tests wherever component interactions require verification.